﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Text.RegularExpressions;
using System.Web.Security;
using ActivateYourGlutes.Infrastructure;
using ActivateYourGlutes.Infrastructure.MembershipServices;
using AutoMapper;
using Account=ActivateYourGlutes.Model.Internal.Account;
using Activity=ActivateYourGlutes.Model.Internal.Activity;
using UnitOfLength=ActivateYourGlutes.Model.Internal.UnitOfLength;
using UnitOfWeight=ActivateYourGlutes.Model.Internal.UnitOfWeight;

namespace ActivateYourGlutes.Model.Providers
{
    public class AccountProvider : MembershipProviderBase, IAccountProvider
    {
        public AccountProvider(
            IEntityModelFactory factory,
            IMembershipService service)
            : base(factory, service)
        {
            
        }

        /// <summary>
        /// Retrieves the current account based on a security principal object 
        /// </summary>
        public Contracts.Account CurrentAccount(IPrincipal user)
        {
            string username = user.Identity.Name;
            var account = Db.AccountSet.Include("UnitOfLength").
                Include("UnitOfWeight").
                Include("PrimaryMeasurementType").
                Include("SecondaryMeasurementType").
                Where(ac => ac.Username == username).FirstOrDefault();

            var contractAccount = Mapper.Map<Account, Contracts.Account>(account);
            if (contractAccount != null)
                contractAccount.IsAdministrator = MembershipService.IsAdministrator(username);
            return contractAccount;
        }

        /// <summary>
        /// Updates the details of an account
        /// </summary>
        public void UpdateAccount(Contracts.Account forAccount, string email, DateTime dateOfBirth, Contracts.UnitOfLength unitOfLength, Contracts.UnitOfWeight unitOfWeight, IEnumerable<Contracts.MeasurementType> keyIndicators)
        {
            int keyIndicatorCount = keyIndicators.Count();
            if (keyIndicatorCount > 2)
                throw new ValidationException("_FORM", "A maximum of two key indicators may be selected.");

            var containerUnitOfLength =
                Db.UnitOfLengthSet.Where(ul => ul.UnitOfLengthId == unitOfLength.UnitOfLengthId).First();
            var containerUnitOfWeight =
                Db.UnitOfWeightSet.Where(uw => uw.UnitOfWeightId == unitOfWeight.UnitOfWeightId).First();
            var account = Db.AccountSet.Where(a => a.AccountId == forAccount.AccountId).First();
            account.DateOfBirth = dateOfBirth;
            account.UnitOfLength = containerUnitOfLength;
            account.UnitOfWeight = containerUnitOfWeight;
            account.PrimaryMeasurementType = null;
            account.SecondaryMeasurementType = null;

            if (keyIndicatorCount > 0)
            {
                account.PrimaryMeasurementTypeReference.EntityKey = new EntityKey("ActivateYourGlutesEntities.MeasurementTypeSet", "MeasurementTypeId", keyIndicators.First().MeasurementTypeId);
                if (keyIndicatorCount == 2)
                    account.SecondaryMeasurementTypeReference.EntityKey = new EntityKey("ActivateYourGlutesEntities.MeasurementTypeSet", "MeasurementTypeId", keyIndicators.Skip(1).First().MeasurementTypeId);
            }

            var errors = account.Validate();
            if (errors != null && errors.Count > 0)
                throw new ValidationException(errors);

            MembershipService.ChangeEmail(forAccount.Username, email);

            Db.SaveChanges();
        }

        /// <summary>
        /// Creates an account
        /// </summary>
        public void Create(string name, string password, string email, DateTime time, Contracts.UnitOfLength length, Contracts.UnitOfWeight weight, string dateFormat)
        {
            MembershipCreateStatus createStatus = MembershipService.CreateUser(name, password, email);
            if (createStatus == MembershipCreateStatus.Success)
            {

                var account = new Account
                                  {
                                      Username = name,
                                      DateOfBirth = new DateTime(1976, 1, 22),
                                      LocalDateFormat = dateFormat,
                                      UnitOfLengthReference =
                                          {
                                              EntityKey =
                                                  new EntityKey("ActivateYourGlutesEntities.UnitOfLengthSet",
                                                                "UnitOfLengthId",
                                                                length.UnitOfLengthId)
                                          },
                                      UnitOfWeightReference =
                                          {
                                              EntityKey =
                                                  new EntityKey("ActivateYourGlutesEntities.UnitOfWeightSet",
                                                                "UnitOfWeightId",
                                                                weight.UnitOfWeightId)
                                          }
                                  };
                var errors = account.Validate();
                if (errors != null && errors.Count > 0)
                {
                    throw new ValidationException(errors);
                }

                Db.AddToAccountSet(account);
                Db.SaveChanges();
            }
            else
            {
                throw new ValidationException("_FORM", ErrorCodeToString(createStatus));
            }

        }

        /// <summary>
        /// Determines if any data has been logged by this user beyond account creation
        /// </summary>
        public bool IsDataLoggedForUser(IPrincipal principal)
        {
            var account = CurrentAccount(principal);
            var firstBody = Db.BodySet.Where(b => b.Account.AccountId == account.AccountId).FirstOrDefault();
            return firstBody != null;
        }

        /// <summary>
        /// Returns the recent activity the user has made against their account
        /// </summary>
        public IEnumerable<Contracts.Activity> RecentActivity(IPrincipal user)
        {
            var account = CurrentAccount(user);
            return Mapper.Map<List<Activity>, List<Contracts.Activity>>(Db.ActivitySet.Include("Body").Where(a => a.Account.AccountId == account.AccountId).OrderByDescending(a => a.ActivityDate).Take(5).ToList()); 
        }

        /// <summary>
        /// Validates the details supplied for logon
        /// </summary>
        public void ValidateLogOn(string userName, string password)
        {
            var errors = new NameValueCollection();
            bool usernameIsEmpty = String.IsNullOrEmpty(userName);
            bool passwordIsEmpty = String.IsNullOrEmpty(password);
            if (usernameIsEmpty || passwordIsEmpty)
            {
                if (usernameIsEmpty)
                {
                    errors.Add("username", "You must specify a username.");
                }
                if (passwordIsEmpty)
                {
                    errors.Add("password", "You must specify a password.");
                }
            }
            else if (!MembershipService.ValidateUser(userName, password))
            {
                errors.Add("_FORM", "The username or password provided is incorrect.");
            }

            if (errors.Count > 0)
                throw new ValidationException(errors);
        }

        /// <summary>
        /// Validates the details supplied for registration
        /// </summary>
        public void ValidateRegistration(string userName, string email, string password, string confirmPassword, bool acceptTerms, DateTime? dob)
        {
            var errors = new NameValueCollection();
            if (dob == null)
            {
                errors.Add("dateOfBirth", "Date of birth is required and must use the format mm-dd-yyyy");
            }
            else
            {
                if (dob.Value.AddYears(18) > new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day))
                    errors.Add("dateOfBirth", "You must be at least 18 years old to use this site.");
            }
            if (String.IsNullOrEmpty(userName))
            {
                errors.Add("username", "You must specify a username.");
            }
            if (String.IsNullOrEmpty(email))
            {
                errors.Add("email", "You must specify an email address.");
            }
            else
            {
                Match match = Regex.Match(email, @"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b", RegexOptions.IgnoreCase);
                if (!match.Success)
                    errors.Add("email", "You must specify a valid email address.");
            }
            if (password == null || password.Length < MembershipService.MinPasswordLength)
            {
                errors.Add("password",
                    String.Format(CultureInfo.CurrentCulture,
                         "You must specify a password of {0} or more characters.",
                         MembershipService.MinPasswordLength));
            }
            if (!String.Equals(password, confirmPassword, StringComparison.Ordinal))
            {
                errors.Add("_FORM", "The new password and confirmation password do not match.");
            }
            if (!acceptTerms)
            {
                errors.Add("acceptTerms", "You must agree to the terms of service and privacy policy.");
            }

            if (errors.Count > 0)
                throw new ValidationException(errors);
        }

        /// <summary>
        /// Validates the details supplied for changing a password
        /// </summary>
        public void ValidateChangePassword(string currentPassword, string newPassword, string confirmPassword)
        {
            var errors = new NameValueCollection();

            if (String.IsNullOrEmpty(currentPassword))
            {
                errors.Add("currentPassword", "You must specify a current password.");
            }
            if (newPassword == null || newPassword.Length < MembershipService.MinPasswordLength)
            {
                errors.Add("newPassword",
                    String.Format(CultureInfo.CurrentCulture,
                         "You must specify a new password of {0} or more characters.",
                         MembershipService.MinPasswordLength));
            }

            if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal))
            {
                errors.Add("_FORM", "The new password and confirmation password do not match.");
            }

            if (errors.Count > 0)
                throw new ValidationException(errors);
        }

        /// <summary>
        /// Returns the email address for an account from the membership provider
        /// </summary>
        public string EmailForAccount(Contracts.Account account)
        {
            return MembershipService.Email(account.Username);
        }

        public int MinPasswordLength
        {
            get { return MembershipService.MinPasswordLength; }
        }

        /// <summary>
        /// Change the password for the given user
        /// </summary>
        public bool ChangePassword(string accountName, string newPassword, string currentPassword, string confirmPassword)
        {
            ValidateChangePassword(currentPassword, newPassword, confirmPassword);
            return MembershipService.ChangePassword(accountName, currentPassword, newPassword);
        }        

        private void SetAdministratorFlag(IEnumerable<Contracts.Account> accounts)
        {
            foreach (var account in accounts)
                account.IsAdministrator = MembershipService.IsAdministrator(account.Username);
        }

        public IEnumerable<Contracts.Account> All(Contracts.Account accessingAccount)
        {
            // don't rely on what we're told!
            AuthorizeAccountAsAdministrator(accessingAccount);
            var accounts = Mapper.Map<IEnumerable<Internal.Account>, IEnumerable<Contracts.Account>>(Db.AccountSet.ToList());
            SetAdministratorFlag(accounts);
            return accounts;
        }

        /// <summary>
        /// Gets the total number of accounts in the system
        /// </summary>
        public int TotalAccounts(Contracts.Account accessingAccount)
        {
            return Db.AccountSet.Count();
        }

        /// <summary>
        /// Returns a subset of all the accounts (used by paging views for example)
        /// </summary>
        public IEnumerable<Contracts.Account> Accounts(Contracts.Account accessingAccount, int offset, int pageSize)
        {
            AuthorizeAccountAsAdministrator(accessingAccount);
            var accounts = Db.AccountSet.Include("UnitOfLength").
                Include("UnitOfWeight").
                Include("PrimaryMeasurementType").
                Include("SecondaryMeasurementType").
                OrderByDescending(a => a.Username).
                Skip(offset).
                Take(pageSize).
                ToList();
            var mappedAccounts = Mapper.Map<IEnumerable<Internal.Account>, IEnumerable<Contracts.Account>>(accounts);
            SetAdministratorFlag(mappedAccounts);
            return mappedAccounts;
        }

        /// <summary>
        /// Gets an account with the given ID after checking that the requesting account has rights
        /// to recieve it.
        /// </summary>
        /// <param name="requestingAccount"></param>
        /// <param name="requestedAccountId"></param>
        /// <returns></returns>
        public Contracts.Account GetAccount(Contracts.Account requestingAccount, int requestedAccountId)
        {
            if (requestingAccount == null)
                throw new AuthorizationException();
            bool isAdministrator = MembershipService.IsAdministrator(requestingAccount.Username);
            if (!isAdministrator && requestedAccountId != requestingAccount.AccountId)
                throw new AuthorizationException("You do not have rights to perform this function");
            var account = Db.AccountSet.Include("UnitOfLength").
                Include("UnitOfWeight").
                Include("PrimaryMeasurementType").
                Include("SecondaryMeasurementType").
                Where(ac => ac.AccountId == requestedAccountId).
                First();

            var mappedAccount = Mapper.Map<Internal.Account, Contracts.Account>(account);
            SetAdministratorFlag(new[] { mappedAccount});
            return mappedAccount;
        }

        public void AdminUpdateAccount(Contracts.Account updatingAccount, int accountId, bool isAdministrator)
        {
            if (updatingAccount == null)
                throw new AuthorizationException();
            if (updatingAccount.AccountId == accountId)
            {
                throw new ValidationException("_FORM", "You cannot alter your own administrator status.");
            }

            bool updatingAccountIsAdministrator = MembershipService.IsAdministrator(updatingAccount.Username);
            if (!updatingAccountIsAdministrator && accountId != updatingAccount.AccountId)
                throw new AuthorizationException("You do not have rights to perform this function");

            var account = Db.AccountSet.Where(a => a.AccountId == accountId).First();
            MembershipService.SetIsAdministrator(account.Username, isAdministrator);
        }

        public void Delete(Contracts.Account deletingAccount, int accountId)
        {
            if (deletingAccount == null)
                throw new AuthorizationException();

            bool updatingAccountIsAdministrator = MembershipService.IsAdministrator(deletingAccount.Username);
            if (!updatingAccountIsAdministrator && accountId != deletingAccount.AccountId)
                throw new AuthorizationException("You do not have rights to perform this function");

            // Activities
            var activities = Db.ActivitySet.Where(a => a.Account.AccountId == accountId).ToList();
            foreach(var activity in activities)
                Db.DeleteObject(activity);
            // Goals
            var goals = Db.GoalSet.Where(g => g.Account.AccountId == accountId).ToList();
            foreach(var goal in goals)
                Db.DeleteObject(goal);            
            // Measurements
            var measurements = Db.MeasurementSet.Where(m => m.Body.Account.AccountId == accountId).ToList();
            foreach(var measurement in measurements)
                Db.DeleteObject(measurement);
            // Bodies
            var bodies = Db.BodySet.Where(b => b.Account.AccountId == accountId).ToList();
            foreach(var body in bodies)
                Db.DeleteObject(body);
            // Custom measurement types
            var measurementTypes = Db.MeasurementTypeSet.Where(mt => mt.UserDefinedTypeOwner.AccountId == accountId);
            foreach(var measurementType in measurementTypes)
                Db.DeleteObject(measurementType);
            // Account
            var account = Db.AccountSet.Where(a => a.AccountId == accountId).First();
            Db.DeleteObject(account);
            // Logon
            MembershipService.DeleteUser(account.Username);

            Db.SaveChanges();
        }

        #region Errors codes

        private static string ErrorCodeToString(MembershipCreateStatus createStatus)
        {
            // See http://msdn.microsoft.com/en-us/library/system.web.security.membershipcreatestatus.aspx for
            // a full list of status codes.
            switch (createStatus)
            {
                case MembershipCreateStatus.DuplicateUserName:
                    return "Username already exists. Please enter a different user name.";

                case MembershipCreateStatus.DuplicateEmail:
                    return "A username for that e-mail address already exists. Please enter a different e-mail address.";

                case MembershipCreateStatus.InvalidPassword:
                    return "The password provided is invalid. Please enter a valid password value.";

                case MembershipCreateStatus.InvalidEmail:
                    return "The e-mail address provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidAnswer:
                    return "The password retrieval answer provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidQuestion:
                    return "The password retrieval question provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidUserName:
                    return "The user name provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.ProviderError:
                    return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

                case MembershipCreateStatus.UserRejected:
                    return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

                default:
                    return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
            }
        }
        #endregion
        
    }
}